library leancloud_rest_api;

import 'dart:convert';
import 'dart:typed_data';

import 'package:convert/convert.dart';
import 'package:crypto/crypto.dart';
import 'package:dio/dio.dart';

/// Encoding & Decoding
part 'internal/codec/lc_decoder.dart';
part 'internal/codec/lc_encoder.dart';

/// http
part "internal/http/lc_app_router.dart";
part "internal/http/lc_app_server.dart";
part "internal/http/lc_http_client.dart";

/// Utilities
part 'internal/utils/lc_utils.dart';

/// Public Interface
part 'lc_base_model.dart';
part 'lc_db_config.dart';
part 'lc_exception.dart';
part 'lc_logger.dart';
part 'lc_query.dart';

const String sDKVersion = '0.7.10';
const String aPIVersion = '1.1';

class LraRestApi<T extends LraBaseModel> {
  final LraDbConfig dbConfig;
  final LraHttpClient _httpClient;
  final String className;
  final T Function(Map<dynamic, dynamic>) fromJson;

  LraRestApi(this.dbConfig, this.className, this.fromJson)
      : _httpClient = LraHttpClient(dbConfig.appId, dbConfig.appKey,
            dbConfig.serverUrl, sDKVersion, aPIVersion);

  /// Counts the number of results.
  Future<int> count(LraQuery query) async {
    String path = 'classes/$className';
    Map<String, dynamic> params = query.buildParams();
    params['limit'] = 0;
    params['count'] = 1;
    Map result = await _httpClient.get(path, queryParams: params);
    return result['count'];
  }

  /// Retrieves a list of [LCObject]s matching this query
  ///
  /// $ne	不等于
  ///
  /// $lt	小于
  ///
  /// $lte	小于等于
  ///
  /// $gt	大于
  ///
  /// $gte	大于等于
  ///
  /// $regex	正则表达式； $options 指定全局修饰符
  ///
  /// $in	包含任意一个数组值
  ///
  /// $nin	不包含任意一个数组值
  ///
  /// $all	包括所有的数组值
  ///
  /// $exists	指定 Key 有值
  ///
  /// $select	匹配另一个查询的返回值
  ///
  /// $dontSelect	排除另一个查询的返回值
  Future<List<T>?> find([LraQuery? query]) async {
    return fetch(query);
  }

  /// Constructs a [LCObject] whose [objectId] is already known.
  Future<T?> get(String objectId,
      {Iterable<String>? keys, Iterable<String>? includes}) async {
    if (isNullOrEmpty(objectId)) {
      throw ArgumentError.notNull('objectId');
    }

    Map<String, dynamic> queryParams = {};
    if (keys != null) {
      queryParams['keys'] = keys.join(',');
    }
    if (includes != null) {
      queryParams['include'] = includes.join(',');
    }

    String path = "classes/$className/$objectId";
    Map<String, dynamic> response =
        await _httpClient.get(path, queryParams: queryParams);
    return _decodeLCObject(response);
  }

  /// Retrieves at most one [LCObject] matching this query.
  Future<T?> first([LraQuery? query]) async {
    query ??= LraQuery();
    query.limit = 1;
    List<T>? results = await find(query);
    if (results != null && results.isNotEmpty) {
      return results.first;
    }
    return null;
  }

  Future create(Map<String, dynamic> newObj,
      [bool fetchWhenSave = false]) async {
    String path =
        'classes/$className${fetchWhenSave ? '?fetchWhenSave=true' : ''}';
    var data = _LCEncoder.encode(newObj);
    Map response = await _httpClient.post(path, data: data);
    return response;
  }

  Future update(Map<String, dynamic> obj, [bool fetchWhenSave = false]) async {
    if (obj['objectId'] == null) {
      throw Exception("objectId must have");
    }
    String path = 'classes/$className/${obj['objectId']}';
    var data = _LCEncoder.encode(obj);
    Map response = await _httpClient.put(path, data: data);
    return response;
  }

  Future delete(String objectId) async {
    if (isNullOrEmpty(objectId)) {
      throw ArgumentError.notNull('objectId');
    }
    String path = 'classes/$className/$objectId';
    await _httpClient.delete(path);
  }

  Future<List<T>> fetch(LraQuery? query) async {
    String path = 'classes/$className';
    query ??= LraQuery();
    Map<String, dynamic> params = query.buildParams();
    Map response = await _httpClient.get(path, queryParams: params);
    List results = response['results'];
    List<T> list = [];
    for (var item in results) {
      T object = _decodeLCObject(item);
      list.add(object);
    }
    return list;
  }

  T _decodeLCObject(Map<dynamic, dynamic> data) {
    var newData = _LCDecoder.decode(data);
    var object = fromJson(newData);
    return object;
  }
}
